/* eslint-disable @typescript-eslint/no-non-null-assertion */
import { dom, EventHandler, Nodes } from '@knno/dom';
import { FormItem } from '../interface';
import { calendarStyle } from './calendar.css';
import { DATA } from '../symbol';
import time from '@knno/time';
import SVGLeft from '@mdi/svg/svg/chevron-left.svg';
import SVGRight from '@mdi/svg/svg/chevron-right.svg';
import { i18n } from '@knno/i18n';
import '../../dict';

export type DateValue = Date | string;
export type CalMode = 'year' | 'month' | 'date';

export interface CalDataBase {
	selected: Date[];
	isRange: boolean;
}

interface CalData extends CalDataBase {
	mode: CalMode;
	time: Date;
	keyboard: boolean;
	min?: Date;
	max?: Date;
}

export function dateValue(store: CalDataBase): Date[] | Date | null;
export function dateValue(store: CalDataBase, val: DateValue[] | DateValue | null): void;
export function dateValue(store: CalDataBase, val?: DateValue[] | DateValue | null): void | Date[] | Date | null {
	if (val === undefined) {
		if (store.isRange) {
			return store.selected;
		} else {
			return store.selected[0] ?? null;
		}
	}
	if (store.isRange) {
		if (val instanceof Array) {
			store.selected = val
				.slice(0, 2)
				.map((v) => (typeof v === 'string' ? time.parse(v) : v))
				.filter((v): v is Date => v !== null);
			if (store.selected.length === 1) {
				store.selected[1] = store.selected[0];
			}
		} else if (val instanceof Date) {
			store.selected = [val, val];
		} else if (typeof val === 'string') {
			const d = time.parse(val);
			store.selected = d === null ? [] : [d, d];
		} else {
			store.selected = [];
		}
	} else {
		if (val instanceof Array) {
			store.selected = val
				.slice(0, 1)
				.map((v) => (typeof v === 'string' ? time.parse(v) : v))
				.filter((v): v is Date => v !== null);
		} else if (val instanceof Date) {
			store.selected = [val];
		} else if (typeof val === 'string') {
			const d = time.parse(val);
			store.selected = d === null ? [] : [d];
		} else {
			store.selected = [];
		}
	}
	store.selected.sort((a, b) => a.getTime() - b.getTime());
}

interface CalendarEventDetail {
	value: string;
}

type CalendarEvent = CustomEvent<CalendarEventDetail>;

export interface CalendarEventMap {
	pick: CalendarEvent;
}

export type CalendarEventKey = keyof CalendarEventMap;
export type CalendarEventHandler<T extends CalendarEventKey, O extends Nodes> = (
	this: O,
	event: CalendarEventMap[T]
) => void;

export interface Calendar {
	on<K extends keyof CalendarEventMap>(
		event: K,
		listener: CalendarEventHandler<K, this>,
		options?: boolean | AddEventListenerOptions
	): this;
	on<K extends keyof HTMLElementEventMap>(
		event: K,
		listener: EventHandler<K, this>,
		options?: boolean | AddEventListenerOptions
	): this;

	off<K extends keyof CalendarEventMap>(
		event: K,
		listener: CalendarEventHandler<K, this>,
		options?: boolean | AddEventListenerOptions
	): this;
	off<K extends keyof HTMLElementEventMap>(
		event: K,
		listener: EventHandler<K, this>,
		options?: boolean | AddEventListenerOptions
	): this;
}

export class Calendar extends Nodes implements FormItem {
	private [DATA]: CalData = {
		keyboard: false,
		selected: [],
		mode: 'date',
		isRange: false,
		time: new Date(),
	};

	constructor() {
		let timer: NodeJS.Timeout | null = null;
		const supportFlip = (elem: Nodes, direction: -1 | 1): Nodes => {
			elem
				.on('pointerup', (evt) => {
					(evt.target as HTMLElement).releasePointerCapture(evt.pointerId);
					timer && clearTimeout(timer);
				})
				.on('pointerdown', (evt) => {
					this[DATA].keyboard = false;
					(evt.target as HTMLElement).setPointerCapture(evt.pointerId);
					const flip = () => {
						this.flip(direction);
						timer = setTimeout(flip, 100);
					};
					timer = setTimeout(flip, 500);
				});
			return elem;
		};
		const bar = dom.div('bar').append(
			supportFlip(
				dom
					.div('btn')
					.html(SVGLeft)
					.css('padding', '0 1em')
					.on('click', () => {
						timer && clearTimeout(timer);
						this.flip(-1);
					}),
				-1
			),
			dom.div('title'),
			supportFlip(
				dom
					.div('btn')
					.html(SVGRight)
					.css('padding', '0 1em')
					.on('click', () => {
						timer && clearTimeout(timer);
						this.flip(1);
					}),
				1
			)
		);
		const root = dom.div(calendarStyle).attr('tabIndex', '0').append(bar, dom.div('body'));
		super(root);
		root.on('keyup', (evt) => {
			if (evt.code === 'Enter') {
				evt.preventDefault();
				evt.stopPropagation();
				if (this[DATA].keyboard) {
					this.select();
					this.render();
					this.emit(new CustomEvent('pick', { detail: { value: this.value() } }));
				}
			}
		});
		root.on('keydown', (evt) => {
			const m = this[DATA].mode;
			if (evt.code === 'ArrowDown') {
				evt.preventDefault();
				evt.stopPropagation();
				this[DATA].keyboard = true;
				if (m === 'year') {
					this.move(3, 'y');
				} else if (m === 'month') {
					this.move(3, 'M');
				} else {
					this.move(7, 'd');
				}
			} else if (evt.code === 'ArrowUp') {
				evt.preventDefault();
				evt.stopPropagation();
				this[DATA].keyboard = true;
				if (m === 'year') {
					this.move(-3, 'y');
				} else if (m === 'month') {
					this.move(-3, 'M');
				} else {
					this.move(-7, 'd');
				}
			} else if (evt.code === 'ArrowLeft') {
				evt.preventDefault();
				evt.stopPropagation();
				this[DATA].keyboard = true;
				if (m === 'year') {
					this.move(-1, 'y');
				} else if (m === 'month') {
					this.move(-1, 'M');
				} else {
					this.move(-1, 'd');
				}
			} else if (evt.code === 'ArrowRight') {
				evt.preventDefault();
				evt.stopPropagation();
				this[DATA].keyboard = true;
				if (m === 'year') {
					this.move(1, 'y');
				} else if (m === 'month') {
					this.move(1, 'M');
				} else {
					this.move(1, 'd');
				}
			} else if (evt.code === 'PageUp') {
				evt.preventDefault();
				evt.stopPropagation();
				this[DATA].keyboard = true;
				this.flip(-1);
			} else if (evt.code === 'PageDown') {
				evt.preventDefault();
				evt.stopPropagation();
				this[DATA].keyboard = true;
				this.flip(1);
			}
		});
		this.render();
	}

	private move(direction: number, unit: 'y' | 'M' | 'd') {
		this[DATA].time = time.add(this[DATA].time, direction, unit)!;
		this.adjust();
		this.render();
	}

	private adjust() {
		const min = this.min();
		if (min && this[DATA].time < min) {
			this[DATA].time = new Date(min);
		}
		const max = this.max();
		if (max && this[DATA].time > max) {
			this[DATA].time = new Date(max);
		}
	}

	private flip(direction: 1 | -1) {
		const m = this[DATA].mode;
		if (m === 'year') {
			this[DATA].time = time.add(this[DATA].time, 10 * direction, 'y')!;
		} else if (m === 'month') {
			this[DATA].time = time.add(this[DATA].time, direction, 'y')!;
		} else {
			this[DATA].time = time.add(this[DATA].time, direction, 'M')!;
		}
		this.adjust();
		this.render();
	}

	private isSelected(date: Date): boolean {
		if (this[DATA].selected.length === 0) return false;
		const arr = this[DATA].selected.slice(0, 2).map((d) => {
			if (this[DATA].mode === 'year') {
				return new Date(d.getFullYear(), 0, 1);
			} else if (this[DATA].mode === 'month') {
				return new Date(d.getFullYear(), d.getMonth(), 1);
			} else {
				return new Date(d.getFullYear(), d.getMonth(), d.getDate());
			}
		});
		if (arr.length === 1) arr.push(arr[0]);
		return date >= arr[0] && date <= arr[1];
	}

	private select() {
		if (this[DATA].isRange && this[DATA].selected.length === 1) {
			this[DATA].selected.push(new Date(this[DATA].time));
			this[DATA].selected.sort((a, b) => a.getTime() - b.getTime());
		} else {
			this[DATA].selected = [new Date(this[DATA].time)];
		}
	}

	private renderYear(body: Nodes) {
		const current = this[DATA].time.getFullYear();
		const year = current - (current % 10) - 1;
		const yearOfToday = new Date().getFullYear();
		this.query('div.bar>div.title').text(year + 1 + ' ~ ' + (year + 10));
		const minYear = this.min()?.getFullYear();
		const maxYear = this.max()?.getFullYear();
		body
			.attr('mode', 'year')
			.html('')
			.append(
				...Array(12)
					.fill(0)
					.map((_, idx) => {
						const t = new Date(year + idx, 0, 1);
						const disabled = (minYear != null && year + idx < minYear) || (maxYear != null && year + idx > maxYear);
						return dom
							.div(
								'c' + idx,
								year + idx === current ? 'active' : '',
								year + idx === yearOfToday ? 'today' : '',
								this.isSelected(t) ? 'selected' : '',
								disabled ? 'disabled' : ''
							)
							.attr('disabled', disabled)
							.text(`${year + idx}`)
							.on('click', () => {
								this[DATA].keyboard = false;
								this[DATA].time = new Date(year + idx, 0, 1);
								this.select();
								this.render();
								this.emit(new CustomEvent('pick', { detail: { value: this.value() } }));
							});
					})
			);
	}

	private renderMonth(body: Nodes) {
		const year = this[DATA].time.getFullYear();
		this.query('div.bar>div.title').text(year + '');
		const today = new Date();
		const yearOfToday = today.getFullYear();
		const monthOfToday = today.getMonth();
		const min = this.min();
		const max = this.max();
		const minTime = min ? new Date(min.getFullYear(), min.getMonth(), 1) : null;
		const maxTime = max ? new Date(max.getFullYear(), max.getMonth(), 1) : null;
		body
			.attr('mode', 'month')
			.html('')
			.append(
				...Array(12)
					.fill(0)
					.map((_, idx) => {
						const t = new Date(year, idx, 1);
						const disabled = (minTime != null && t < minTime) || (maxTime != null && t > maxTime);
						return dom
							.div(
								'c' + idx,
								year === this[DATA].time.getFullYear() && idx === this[DATA].time.getMonth() ? 'active' : '',
								year === yearOfToday && idx === monthOfToday ? 'today' : '',
								this.isSelected(t) ? 'selected' : '',
								disabled ? 'disabled' : ''
							)
							.attr('disabled', disabled)
							.text(i18n(time.nameOfMonth(idx, true)))
							.on('click', () => {
								this[DATA].keyboard = false;
								this[DATA].time = new Date(year, idx, 1);
								this.select();
								this.render();
								this.emit(new CustomEvent('pick', { detail: { value: this.value() } }));
							});
					})
			);
	}

	private renderDay(body: Nodes) {
		this.query('div.bar>div.title').text(time.format(this[DATA].time, 'yyyy - M')!);
		const monthStart = new Date(this[DATA].time.getFullYear(), this[DATA].time.getMonth(), 1);
		const nextMonthStart = time.add(monthStart, 1, 'M')!;
		const begin = time.add(monthStart, -monthStart.getDay(), 'd')!;
		const today = new Date();
		const t = new Date(this[DATA].time.getFullYear(), this[DATA].time.getMonth(), this[DATA].time.getDate());
		const min = this.min();
		const max = this.max();
		const minTime = min ? new Date(min.getFullYear(), min.getMonth(), min.getDate()) : null;
		const maxTime = max ? new Date(max.getFullYear(), max.getMonth(), max.getDate()) : null;
		body
			.attr('mode', 'day')
			.html('')
			.append(
				...Array(7)
					.fill(0)
					.map((_, idx) => {
						return dom.div('week').text(i18n(time.nameOfweek(idx, true).substring(0, 2)));
					}),
				...Array(42)
					.fill(0)
					.map((_, idx) => {
						const day = time.add(begin, idx, 'd')!;
						const disabled = (minTime != null && day < minTime) || (maxTime != null && day > maxTime);
						return dom
							.div(
								day < monthStart || day >= nextMonthStart ? 'out' : '',
								day.getFullYear() === t.getFullYear() &&
									day.getMonth() === t.getMonth() &&
									day.getDate() === t.getDate()
									? 'active'
									: '',
								this.isSelected(day) ? 'selected' : '',
								day.getFullYear() === today.getFullYear() &&
									day.getMonth() === today.getMonth() &&
									day.getDate() === today.getDate()
									? 'today'
									: '',
								disabled ? 'disabled' : ''
							)
							.attr('disabled', disabled)
							.text(`${day.getDate()}`)
							.on('click', () => {
								this[DATA].keyboard = false;
								t.setMonth(day.getMonth());
								t.setDate(day.getDate());
								this[DATA].time = t;
								this.select();
								this.render();
								this.emit(new CustomEvent('pick', { detail: { value: this.value() } }));
							});
					})
			);
	}

	private render() {
		const body = this.query('div.body');
		const mode = this[DATA].mode;
		body.attr('keyboard', this[DATA].keyboard);
		if (mode === 'year') {
			this.renderYear(body);
		} else if (mode === 'month') {
			this.renderMonth(body);
		} else {
			this.renderDay(body);
		}
	}

	mode(): CalMode;
	mode(value: CalMode): this;
	mode(value?: CalMode): CalMode | this {
		if (value === undefined) {
			return this[DATA].mode;
		}
		this[DATA].mode = value;
		this.render();
		return this;
	}

	min(): Date | null;
	min(value: Date | null): this;
	min(value?: Date | null): Date | null | this {
		if (value === undefined) {
			return this[DATA].min ?? null;
		}
		if (value) {
			this[DATA].min = new Date(value);
		} else delete this[DATA].min;
		this.render();
		return this;
	}

	max(): Date | null;
	max(value: Date | null): this;
	max(value?: Date | null): Date | null | this {
		if (value === undefined) {
			return this[DATA].max ?? null;
		}
		if (value) this[DATA].max = value;
		else delete this[DATA].max;
		this.render();
		return this;
	}

	override value(): Date[] | Date | null;
	override value(val: DateValue[] | DateValue | null): this;
	override value(val?: DateValue[] | DateValue | null): this | Date[] | Date | null {
		if (val === undefined) {
			return dateValue(this[DATA]);
		}
		dateValue(this[DATA], val);
		if (this[DATA].selected.length > 0) {
			this[DATA].time = this[DATA].selected[0];
		}
		this.render();
		return this;
	}

	override text(): string;
	override text(value: string): this;
	override text(value?: string): string | this {
		if (value === undefined) {
			if (this[DATA].selected.length === 0) return '';
			let pattern: string;
			if (this[DATA].mode === 'year') {
				pattern = 'yyyy';
			} else if (this[DATA].mode === 'month') {
				pattern = 'yyyy-MM';
			} else {
				pattern = 'yyyy-MM-dd';
			}
			const arr = this[DATA].selected.slice(0, 2);
			if (this[DATA].isRange && arr.length === 1) {
				arr.push(arr[0]);
			}
			return arr.map((d) => time.format(d, pattern)).join(' ~ ');
		}
		return this.value(value || null);
	}

	range(): boolean;
	range(value: boolean): this;
	range(value?: boolean): this | boolean {
		if (value === undefined) {
			return this[DATA].isRange;
		}
		this[DATA].isRange = value;
		this.value(this.value());
		return this;
	}

	disabled(): boolean;
	disabled(value: boolean): this;
	disabled(value?: boolean): boolean | this {
		if (value === undefined) {
			return this.attr('disabled') != null;
		}
		return this.attr('disabled', value);
	}
}

export function calendar(...className: string[]): Calendar {
	return new Calendar().addClass(...className);
}
